@mermaid-js/mermaid-cli 11.9.0 → 11.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-types/src/index.d.ts +10 -2
- package/dist-types/src/index.d.ts.map +1 -1
- package/dist-types/src/version.d.ts +1 -1
- package/dist-types/src/version.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.js +28 -6
- package/src/index.js.bak +540 -0
- package/src/version.js +1 -1
|
@@ -30,6 +30,13 @@ export type ParseMDDOptions = {
|
|
|
30
30
|
* - Icon packages to use.
|
|
31
31
|
*/
|
|
32
32
|
iconPacks?: string[] | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* - IconPack Json file name and url to use.
|
|
35
|
+
*
|
|
36
|
+
* /**
|
|
37
|
+
* Render a mermaid diagram.
|
|
38
|
+
*/
|
|
39
|
+
iconPacksNamesAndUrls?: string[] | undefined;
|
|
33
40
|
};
|
|
34
41
|
/**
|
|
35
42
|
* Markdown image properties
|
|
@@ -81,7 +88,8 @@ export function run(input: `${string}.${"md" | "markdown"}` | string | undefined
|
|
|
81
88
|
* @property {boolean} [pdfFit] - If set, scale PDF to fit chart.
|
|
82
89
|
* @property {string} [svgId] - The id attribute for the SVG element to be rendered.
|
|
83
90
|
* @property {string[]} [iconPacks] - Icon packages to use.
|
|
84
|
-
|
|
91
|
+
* @property {string[]} [iconPacksNamesAndUrls] - IconPack Json file name and url to use.
|
|
92
|
+
|
|
85
93
|
/**
|
|
86
94
|
* Render a mermaid diagram.
|
|
87
95
|
*
|
|
@@ -92,7 +100,7 @@ export function run(input: `${string}.${"md" | "markdown"}` | string | undefined
|
|
|
92
100
|
* @returns {Promise<{title: string | null, desc: string | null, data: Uint8Array}>} The output file in bytes,
|
|
93
101
|
* with optional metadata.
|
|
94
102
|
*/
|
|
95
|
-
export function renderMermaid(browser: import("puppeteer").Browser | import("puppeteer").BrowserContext, definition: string, outputFormat: "svg" | "png" | "pdf", { viewport, backgroundColor, mermaidConfig, myCSS, pdfFit, svgId, iconPacks }?: ParseMDDOptions): Promise<{
|
|
103
|
+
export function renderMermaid(browser: import("puppeteer").Browser | import("puppeteer").BrowserContext, definition: string, outputFormat: "svg" | "png" | "pdf", { viewport, backgroundColor, mermaidConfig, myCSS, pdfFit, svgId, iconPacks, iconPacksNamesAndUrls }?: ParseMDDOptions): Promise<{
|
|
96
104
|
title: string | null;
|
|
97
105
|
desc: string | null;
|
|
98
106
|
data: Uint8Array;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;SAsZc,MAAM;;;;SACN,MAAM;;;;;;AAqBpB;;;;;;;;;;;;;;;GAeG;AACH,2BAbW,GAAG,MAAM,IAAI,IAAI,GAAG,UAAU,EAAE,GAAG,MAAM,GAAG,SAAS,UAIrD,GAAG,MAAM,IAAI,IAAI,GAAG,UAAU,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,EAAE,GAAG,aAAa,yEAEhF;IAAiD,eAAe;IACzC,KAAK;IACS,YAAY;IAC3B,SAAS;IAEA,eAAe;CAChD,iBAoHA;AA7UD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,uCAPW,OAAO,WAAW,EAAE,OAAO,GAAG,OAAO,WAAW,EAAE,cAAc,cAChE,MAAM,gBACN,KAAK,GAAG,KAAK,GAAG,KAAK,yGACrB,eAAe,GACb,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAC,CAAC,CA6JlF;AAzSD,qCAwHC;AA5MD;;;;;GAKG;AACH,+BAHW,MAAM,GACJ,KAAK,CAKjB;sBAxBqB,WAAW"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const version: "11.
|
|
1
|
+
export const version: "11.12.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../src/version.js"],"names":[],"mappings":"AAAA,sBAAuB,
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../src/version.js"],"names":[],"mappings":"AAAA,sBAAuB,SAAS,CAAA"}
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -122,11 +122,12 @@ async function cli () {
|
|
|
122
122
|
.option('-q, --quiet', 'Suppress log output')
|
|
123
123
|
.option('-p --puppeteerConfigFile [puppeteerConfigFile]', 'JSON configuration file for puppeteer.')
|
|
124
124
|
.option('--iconPacks <icons...>', 'Icon packs to use, e.g. @iconify-json/logos. These should be Iconify NPM packages that expose a icons.json file, see https://iconify.design/docs/icons/json.html. These will be downloaded from https://unkpg.com when needed.', [])
|
|
125
|
+
.option('--iconPacksNamesAndUrls <prefix#iconsurl...>', 'Icon packs to use, e.g. azure#https://raw.githubusercontent.com/NakayamaKento/AzureIcons/refs/heads/main/icons.json where the name (prefix) of the icon pack is defined before the "#" and the url of the json definition after the "#". These should be Iconify json file formatted as IconifyJson, see https://iconify.design/docs/icons/json.html. These will be downloaded when needed.', [])
|
|
125
126
|
.parse(process.argv)
|
|
126
127
|
|
|
127
128
|
const options = commander.opts()
|
|
128
129
|
|
|
129
|
-
let { theme, width, height, input, output, outputFormat, backgroundColor, configFile, cssFile, svgId, puppeteerConfigFile, scale, pdfFit, quiet, iconPacks, artefacts } = options
|
|
130
|
+
let { theme, width, height, input, output, outputFormat, backgroundColor, configFile, cssFile, svgId, puppeteerConfigFile, scale, pdfFit, quiet, iconPacks, iconPacksNamesAndUrls, artefacts } = options
|
|
130
131
|
|
|
131
132
|
// check input file
|
|
132
133
|
if (!input) {
|
|
@@ -216,7 +217,7 @@ async function cli () {
|
|
|
216
217
|
quiet,
|
|
217
218
|
outputFormat,
|
|
218
219
|
parseMMDOptions: {
|
|
219
|
-
mermaidConfig, backgroundColor, myCSS, pdfFit, viewport: { width, height, deviceScaleFactor: scale }, svgId, iconPacks
|
|
220
|
+
mermaidConfig, backgroundColor, myCSS, pdfFit, viewport: { width, height, deviceScaleFactor: scale }, svgId, iconPacks, iconPacksNamesAndUrls
|
|
220
221
|
},
|
|
221
222
|
artefacts
|
|
222
223
|
}
|
|
@@ -232,7 +233,7 @@ async function cli () {
|
|
|
232
233
|
* @property {boolean} [pdfFit] - If set, scale PDF to fit chart.
|
|
233
234
|
* @property {string} [svgId] - The id attribute for the SVG element to be rendered.
|
|
234
235
|
* @property {string[]} [iconPacks] - Icon packages to use.
|
|
235
|
-
|
|
236
|
+
* @property {string[]} [iconPacksNamesAndUrls] - IconPack Json file name and url to use.
|
|
236
237
|
|
|
237
238
|
/**
|
|
238
239
|
* Render a mermaid diagram.
|
|
@@ -244,7 +245,7 @@ async function cli () {
|
|
|
244
245
|
* @returns {Promise<{title: string | null, desc: string | null, data: Uint8Array}>} The output file in bytes,
|
|
245
246
|
* with optional metadata.
|
|
246
247
|
*/
|
|
247
|
-
async function renderMermaid (browser, definition, outputFormat, { viewport, backgroundColor = 'white', mermaidConfig = {}, myCSS, pdfFit, svgId, iconPacks = [] } = {}) {
|
|
248
|
+
async function renderMermaid (browser, definition, outputFormat, { viewport, backgroundColor = 'white', mermaidConfig = {}, myCSS, pdfFit, svgId, iconPacks = [], iconPacksNamesAndUrls = [] } = {}) {
|
|
248
249
|
const page = await browser.newPage()
|
|
249
250
|
page.on('console', (msg) => {
|
|
250
251
|
console.warn(msg.text())
|
|
@@ -262,7 +263,7 @@ async function renderMermaid (browser, definition, outputFormat, { viewport, bac
|
|
|
262
263
|
page.addScriptTag({ path: mermaidIIFEPath }),
|
|
263
264
|
page.addScriptTag({ path: zenumlIIFEPath })
|
|
264
265
|
])
|
|
265
|
-
const metadata = await page.$eval('#container', async (container, definition, mermaidConfig, myCSS, backgroundColor, svgId, iconPacks) => {
|
|
266
|
+
const metadata = await page.$eval('#container', async (container, definition, mermaidConfig, myCSS, backgroundColor, svgId, iconPacks, iconPacksNamesAndUrls) => {
|
|
266
267
|
await Promise.all(Array.from(document.fonts, (font) => font.load()))
|
|
267
268
|
|
|
268
269
|
/**
|
|
@@ -278,6 +279,7 @@ async function renderMermaid (browser, definition, outputFormat, { viewport, bac
|
|
|
278
279
|
await mermaid.registerExternalDiagrams([zenuml])
|
|
279
280
|
mermaid.registerLayoutLoaders(elkLayouts)
|
|
280
281
|
// lazy load icon packs
|
|
282
|
+
|
|
281
283
|
mermaid.registerIconPacks(
|
|
282
284
|
iconPacks.map((icon) => ({
|
|
283
285
|
name: icon.split('/')[1],
|
|
@@ -287,6 +289,26 @@ async function renderMermaid (browser, definition, outputFormat, { viewport, bac
|
|
|
287
289
|
.catch(() => error(`Failed to fetch icon: ${icon}`))
|
|
288
290
|
}))
|
|
289
291
|
)
|
|
292
|
+
|
|
293
|
+
mermaid.registerIconPacks(
|
|
294
|
+
iconPacksNamesAndUrls.map((iconPackInfo) =>
|
|
295
|
+
{
|
|
296
|
+
var packName = iconPackInfo.split('#')[0];
|
|
297
|
+
var packUrl = iconPackInfo.split('#')[1];
|
|
298
|
+
|
|
299
|
+
return ({
|
|
300
|
+
name: packName,
|
|
301
|
+
loader: () =>
|
|
302
|
+
fetch(packUrl)
|
|
303
|
+
.then((res) => res.json())
|
|
304
|
+
.catch(() => {
|
|
305
|
+
error(`Failed to fetch icon: ${iconPackInfo}`);
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
)
|
|
311
|
+
)
|
|
290
312
|
mermaid.initialize({ startOnLoad: false, ...mermaidConfig })
|
|
291
313
|
// should throw an error if mmd diagram is invalid
|
|
292
314
|
const { svg: svgText } = await mermaid.render(svgId || 'my-svg', definition, container)
|
|
@@ -325,7 +347,7 @@ async function renderMermaid (browser, definition, outputFormat, { viewport, bac
|
|
|
325
347
|
return {
|
|
326
348
|
title, desc
|
|
327
349
|
}
|
|
328
|
-
}, definition, mermaidConfig, myCSS, backgroundColor, svgId, iconPacks)
|
|
350
|
+
}, definition, mermaidConfig, myCSS, backgroundColor, svgId, iconPacks, iconPacksNamesAndUrls)
|
|
329
351
|
|
|
330
352
|
if (outputFormat === 'svg') {
|
|
331
353
|
const svgXML = await page.$eval('svg', (svg) => {
|
package/src/index.js.bak
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import { Command, Option, InvalidArgumentError } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import { resolve } from 'import-meta-resolve'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
import puppeteer from 'puppeteer'
|
|
7
|
+
import url from 'url'
|
|
8
|
+
import { version } from './version.js'
|
|
9
|
+
|
|
10
|
+
// __dirname is not available in ESM modules by default
|
|
11
|
+
const __dirname = url.fileURLToPath(new url.URL('.', import.meta.url))
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Mermaid.js IFFE path.
|
|
15
|
+
*
|
|
16
|
+
* Importing this in a browser adds a global `mermaid` object.
|
|
17
|
+
*/
|
|
18
|
+
const mermaidIIFEPath = path.resolve(path.dirname(url.fileURLToPath(resolve('mermaid', import.meta.url))), 'mermaid.js')
|
|
19
|
+
const zenumlIIFEPath = path.resolve(path.dirname(url.fileURLToPath(resolve('@mermaid-js/mermaid-zenuml', import.meta.url))), 'mermaid-zenuml.js')
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Prints an error to stderr, then closes with exit code 1
|
|
23
|
+
*
|
|
24
|
+
* @param {string} message - The message to print to `stderr`.
|
|
25
|
+
* @returns {never} Quits Node.JS, so never returns.
|
|
26
|
+
*/
|
|
27
|
+
const error = message => {
|
|
28
|
+
console.error(chalk.red(`\n${message}\n`))
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Prints a warning to stderr.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} message - The message to print to `stderr`.
|
|
36
|
+
*/
|
|
37
|
+
const warn = message => {
|
|
38
|
+
console.warn(chalk.yellow(`\n${message}\n`))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Checks if the given file exists.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} file - The file to check.
|
|
45
|
+
* @returns {never | void} If the file doesn't exist, closes Node.JS with
|
|
46
|
+
* exit code 1.
|
|
47
|
+
*/
|
|
48
|
+
const checkConfigFile = file => {
|
|
49
|
+
if (!fs.existsSync(file)) {
|
|
50
|
+
error(`Configuration file "${file}" doesn't exist`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Gets the data in the given file.
|
|
56
|
+
*
|
|
57
|
+
* @param {string | undefined} inputFile - The file to read.
|
|
58
|
+
* If `undefined`, reads from `stdin` instead.
|
|
59
|
+
* @returns {Promise<string>} The contents of `inputFile` parsed as `utf8`.
|
|
60
|
+
*/
|
|
61
|
+
async function getInputData (inputFile) {
|
|
62
|
+
// if an input file has been specified using '-i', it takes precedence over
|
|
63
|
+
// piping from stdin
|
|
64
|
+
if (typeof inputFile !== 'undefined') {
|
|
65
|
+
return await fs.promises.readFile(inputFile, 'utf-8')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return await new Promise((resolve, reject) => {
|
|
69
|
+
let data = ''
|
|
70
|
+
process.stdin.on('readable', function () {
|
|
71
|
+
const chunk = process.stdin.read()
|
|
72
|
+
|
|
73
|
+
if (chunk !== null) {
|
|
74
|
+
data += chunk
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
process.stdin.on('error', function (err) {
|
|
79
|
+
reject(err)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
process.stdin.on('end', function () {
|
|
83
|
+
resolve(data)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Commander parser that converts a string to an integer.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} value - The value from commander.
|
|
92
|
+
* @param {*} _unused - Unused.
|
|
93
|
+
* @returns {number} The value parsed as a number.
|
|
94
|
+
* @throws {InvalidArgumentError} If the arg is not valid.
|
|
95
|
+
* @see https://github.com/tj/commander.js/wiki/Class:-Option#argparserfn
|
|
96
|
+
*/
|
|
97
|
+
function parseCommanderInt (value, _unused) {
|
|
98
|
+
const parsedValue = parseInt(value, 10)
|
|
99
|
+
if (isNaN(parsedValue) || parsedValue < 1) {
|
|
100
|
+
throw new InvalidArgumentError('Not a positive integer.')
|
|
101
|
+
}
|
|
102
|
+
return parsedValue
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function cli () {
|
|
106
|
+
const commander = new Command()
|
|
107
|
+
commander
|
|
108
|
+
.version(version)
|
|
109
|
+
.addOption(new Option('-t, --theme [theme]', 'Theme of the chart').choices(['default', 'forest', 'dark', 'neutral']).default('default'))
|
|
110
|
+
.addOption(new Option('-w, --width [width]', 'Width of the page').argParser(parseCommanderInt).default(800))
|
|
111
|
+
.addOption(new Option('-H, --height [height]', 'Height of the page').argParser(parseCommanderInt).default(600))
|
|
112
|
+
.option('-i, --input <input>', 'Input mermaid file. Files ending in .md will be treated as Markdown and all charts (e.g. ```mermaid (...)``` or :::mermaid (...):::) will be extracted and generated. Use `-` to read from stdin.')
|
|
113
|
+
.option('-o, --output [output]', 'Output file. It should be either md, svg, png, pdf or use `-` to output to stdout. Optional. Default: input + ".svg"')
|
|
114
|
+
.option('-a, --artefacts [artefacts]', 'Output artefacts path. Only used with Markdown input file. Optional. Default: output directory')
|
|
115
|
+
.addOption(new Option('-e, --outputFormat [format]', 'Output format for the generated image.').choices(['svg', 'png', 'pdf']).default(null, 'Loaded from the output file extension'))
|
|
116
|
+
.addOption(new Option('-b, --backgroundColor [backgroundColor]', 'Background color for pngs/svgs (not pdfs). Example: transparent, red, \'#F0F0F0\'.').default('white'))
|
|
117
|
+
.option('-c, --configFile [configFile]', 'JSON configuration file for mermaid.')
|
|
118
|
+
.option('-C, --cssFile [cssFile]', 'CSS file for the page.')
|
|
119
|
+
.option('-I, --svgId [svgId]', 'The id attribute for the SVG element to be rendered.')
|
|
120
|
+
.addOption(new Option('-s, --scale [scale]', 'Puppeteer scale factor').argParser(parseCommanderInt).default(1))
|
|
121
|
+
.option('-f, --pdfFit', 'Scale PDF to fit chart')
|
|
122
|
+
.option('-q, --quiet', 'Suppress log output')
|
|
123
|
+
.option('-p --puppeteerConfigFile [puppeteerConfigFile]', 'JSON configuration file for puppeteer.')
|
|
124
|
+
.option('--iconPacks <icons...>', 'Icon packs to use, e.g. @iconify-json/logos. These should be Iconify NPM packages that expose a icons.json file, see https://iconify.design/docs/icons/json.html. These will be downloaded from https://unkpg.com when needed.', [])
|
|
125
|
+
.parse(process.argv)
|
|
126
|
+
|
|
127
|
+
const options = commander.opts()
|
|
128
|
+
|
|
129
|
+
let { theme, width, height, input, output, outputFormat, backgroundColor, configFile, cssFile, svgId, puppeteerConfigFile, scale, pdfFit, quiet, iconPacks, artefacts } = options
|
|
130
|
+
|
|
131
|
+
// check input file
|
|
132
|
+
if (!input) {
|
|
133
|
+
warn('No input file specified, reading from stdin. ' +
|
|
134
|
+
'If you want to specify an input file, please use `-i <input>.` ' +
|
|
135
|
+
'You can use `-i -` to read from stdin and to suppress this warning.'
|
|
136
|
+
)
|
|
137
|
+
} else if (input === '-') {
|
|
138
|
+
// `--input -` means read from stdin, but suppress the above warning
|
|
139
|
+
input = undefined
|
|
140
|
+
} else if (!fs.existsSync(input)) {
|
|
141
|
+
error(`Input file "${input}" doesn't exist`)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// check output file
|
|
145
|
+
if (!output) {
|
|
146
|
+
// if an input file is defined, it should take precedence, otherwise, input is
|
|
147
|
+
// coming from stdin and just name the file out.svg, if it hasn't been
|
|
148
|
+
// specified with the '-o' option
|
|
149
|
+
if (outputFormat) {
|
|
150
|
+
output = input ? (`${input}.${outputFormat}`) : `out.${outputFormat}`
|
|
151
|
+
} else {
|
|
152
|
+
output = input ? (`${input}.svg`) : 'out.svg'
|
|
153
|
+
}
|
|
154
|
+
} else if (output === '-') {
|
|
155
|
+
// `--output -` means write to stdout.
|
|
156
|
+
output = '/dev/stdout'
|
|
157
|
+
quiet = true
|
|
158
|
+
|
|
159
|
+
if (!outputFormat) {
|
|
160
|
+
outputFormat = 'svg'
|
|
161
|
+
warn('No output format specified, using svg. ' +
|
|
162
|
+
'If you want to specify an output format and suppress this warning, ' +
|
|
163
|
+
'please use `-e <format>.` '
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
} else if (!/\.(?:svg|png|pdf|md|markdown)$/.test(output)) {
|
|
167
|
+
error('Output file must end with ".md"/".markdown", ".svg", ".png" or ".pdf"')
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (artefacts) {
|
|
171
|
+
if (!input || !/\.(?:md|markdown)$/.test(input)) {
|
|
172
|
+
error('Artefacts [-a|--artefacts] path can only be used with Markdown input file')
|
|
173
|
+
}
|
|
174
|
+
if (!fs.existsSync(artefacts)) {
|
|
175
|
+
fs.mkdirSync(artefacts, { recursive: true })
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const outputDir = path.dirname(output)
|
|
180
|
+
if (output !== '/dev/stdout' && !fs.existsSync(outputDir)) {
|
|
181
|
+
error(`Output directory "${outputDir}/" doesn't exist`)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// check config files
|
|
185
|
+
let mermaidConfig = { theme }
|
|
186
|
+
if (configFile) {
|
|
187
|
+
checkConfigFile(configFile)
|
|
188
|
+
mermaidConfig = Object.assign(mermaidConfig, JSON.parse(fs.readFileSync(configFile, 'utf-8')))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let puppeteerConfig = /** @type {import('puppeteer').PuppeteerLaunchOptions} */ ({
|
|
192
|
+
/*
|
|
193
|
+
* `headless: 'shell'` is not officially supported in Puppeteer v19, v20, v21,
|
|
194
|
+
* but still works. In Puppeteer v22, it uses the `chrome-headless-shell` package,
|
|
195
|
+
* which is much faster than the regular headless mode.
|
|
196
|
+
*/
|
|
197
|
+
headless: 'shell'
|
|
198
|
+
})
|
|
199
|
+
if (puppeteerConfigFile) {
|
|
200
|
+
checkConfigFile(puppeteerConfigFile)
|
|
201
|
+
puppeteerConfig = Object.assign(puppeteerConfig, JSON.parse(fs.readFileSync(puppeteerConfigFile, 'utf-8')))
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// check cssFile
|
|
205
|
+
let myCSS
|
|
206
|
+
if (cssFile) {
|
|
207
|
+
if (!fs.existsSync(cssFile)) {
|
|
208
|
+
error(`CSS file "${cssFile}" doesn't exist`)
|
|
209
|
+
}
|
|
210
|
+
myCSS = fs.readFileSync(cssFile, 'utf-8')
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
await run(
|
|
214
|
+
input, output, {
|
|
215
|
+
puppeteerConfig,
|
|
216
|
+
quiet,
|
|
217
|
+
outputFormat,
|
|
218
|
+
parseMMDOptions: {
|
|
219
|
+
mermaidConfig, backgroundColor, myCSS, pdfFit, viewport: { width, height, deviceScaleFactor: scale }, svgId, iconPacks
|
|
220
|
+
},
|
|
221
|
+
artefacts
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @typedef {Object} ParseMDDOptions Options to pass to {@link parseMMD}
|
|
228
|
+
* @property {import("puppeteer").Viewport} [viewport] - Puppeteer viewport (e.g. `width`, `height`, `deviceScaleFactor`)
|
|
229
|
+
* @property {string | "transparent"} [backgroundColor] - Background color.
|
|
230
|
+
* @property {Parameters<import("mermaid")["default"]["initialize"]>[0]} [mermaidConfig] - Mermaid config.
|
|
231
|
+
* @property {CSSStyleDeclaration["cssText"]} [myCSS] - Optional CSS text.
|
|
232
|
+
* @property {boolean} [pdfFit] - If set, scale PDF to fit chart.
|
|
233
|
+
* @property {string} [svgId] - The id attribute for the SVG element to be rendered.
|
|
234
|
+
* @property {string[]} [iconPacks] - Icon packages to use.
|
|
235
|
+
*/
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Render a mermaid diagram.
|
|
239
|
+
*
|
|
240
|
+
* @param {import("puppeteer").Browser | import("puppeteer").BrowserContext} browser - Puppeteer Browser
|
|
241
|
+
* @param {string} definition - Mermaid diagram definition
|
|
242
|
+
* @param {"svg" | "png" | "pdf"} outputFormat - Mermaid output format.
|
|
243
|
+
* @param {ParseMDDOptions} [opt] - Options, see {@link ParseMDDOptions} for details.
|
|
244
|
+
* @returns {Promise<{title: string | null, desc: string | null, data: Uint8Array}>} The output file in bytes,
|
|
245
|
+
* with optional metadata.
|
|
246
|
+
*/
|
|
247
|
+
async function renderMermaid (browser, definition, outputFormat, { viewport, backgroundColor = 'white', mermaidConfig = {}, myCSS, pdfFit, svgId, iconPacks = [] } = {}) {
|
|
248
|
+
const page = await browser.newPage()
|
|
249
|
+
page.on('console', (msg) => {
|
|
250
|
+
console.warn(msg.text())
|
|
251
|
+
})
|
|
252
|
+
try {
|
|
253
|
+
if (viewport) {
|
|
254
|
+
await page.setViewport(viewport)
|
|
255
|
+
}
|
|
256
|
+
const mermaidHTMLPath = path.join(__dirname, '..', 'dist', 'index.html')
|
|
257
|
+
await page.goto(url.pathToFileURL(mermaidHTMLPath).href)
|
|
258
|
+
await page.$eval('body', (body, backgroundColor) => {
|
|
259
|
+
body.style.background = backgroundColor
|
|
260
|
+
}, backgroundColor)
|
|
261
|
+
await Promise.all([
|
|
262
|
+
page.addScriptTag({ path: mermaidIIFEPath }),
|
|
263
|
+
page.addScriptTag({ path: zenumlIIFEPath })
|
|
264
|
+
])
|
|
265
|
+
const metadata = await page.$eval('#container', async (container, definition, mermaidConfig, myCSS, backgroundColor, svgId, iconPacks) => {
|
|
266
|
+
await Promise.all(Array.from(document.fonts, (font) => font.load()))
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* @typedef {Object} GlobalThisWithMermaid
|
|
270
|
+
* We've already imported these modules in our `index.html` file (or by running `page.addScriptTag`),
|
|
271
|
+
* so that they get correctly bundled.
|
|
272
|
+
* @property {import("mermaid")["default"]} mermaid Already imported mermaid instance
|
|
273
|
+
* @property {import("@mermaid-js/mermaid-zenuml")["default"]} mermaid-zenuml Already imported mermaid-zenuml instance
|
|
274
|
+
* @property {import("@mermaid-js/layout-elk")["default"]} elkLayouts Already imported mermaid-elkLayouts instance
|
|
275
|
+
*/
|
|
276
|
+
const { mermaid, 'mermaid-zenuml': zenuml, elkLayouts } = /** @type {GlobalThisWithMermaid & typeof globalThis} */ (globalThis)
|
|
277
|
+
|
|
278
|
+
await mermaid.registerExternalDiagrams([zenuml])
|
|
279
|
+
mermaid.registerLayoutLoaders(elkLayouts)
|
|
280
|
+
// lazy load icon packs
|
|
281
|
+
mermaid.registerIconPacks(
|
|
282
|
+
iconPacks.map((icon) => ({
|
|
283
|
+
name: icon.split('/')[1],
|
|
284
|
+
loader: () =>
|
|
285
|
+
fetch(`https://unpkg.com/${icon}/icons.json`)
|
|
286
|
+
.then((res) => res.json())
|
|
287
|
+
.catch(() => error(`Failed to fetch icon: ${icon}`))
|
|
288
|
+
}))
|
|
289
|
+
)
|
|
290
|
+
mermaid.initialize({ startOnLoad: false, ...mermaidConfig })
|
|
291
|
+
// should throw an error if mmd diagram is invalid
|
|
292
|
+
const { svg: svgText } = await mermaid.render(svgId || 'my-svg', definition, container)
|
|
293
|
+
container.innerHTML = svgText
|
|
294
|
+
|
|
295
|
+
const svg = container.getElementsByTagName?.('svg')?.[0]
|
|
296
|
+
if (svg?.style) {
|
|
297
|
+
svg.style.backgroundColor = backgroundColor
|
|
298
|
+
} else {
|
|
299
|
+
warn('svg not found. Not applying background color.')
|
|
300
|
+
}
|
|
301
|
+
if (myCSS) {
|
|
302
|
+
// add CSS as a <svg>...<style>... element
|
|
303
|
+
// see https://developer.mozilla.org/en-US/docs/Web/API/SVGStyleElement
|
|
304
|
+
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style')
|
|
305
|
+
style.appendChild(document.createTextNode(myCSS))
|
|
306
|
+
svg.appendChild(style)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Finds SVG metadata for accessibility purposes
|
|
310
|
+
/** SVG title */
|
|
311
|
+
let title = null
|
|
312
|
+
// If <title> exists, it must be the first child Node,
|
|
313
|
+
// see https://www.w3.org/TR/SVG11/struct.html#DescriptionAndTitleElements
|
|
314
|
+
/* global SVGTitleElement, SVGDescElement */ // These exist in browser-based code
|
|
315
|
+
if (svg.firstChild instanceof SVGTitleElement) {
|
|
316
|
+
title = svg.firstChild.textContent
|
|
317
|
+
}
|
|
318
|
+
/** SVG description. According to SVG spec, we should use the first one we find */
|
|
319
|
+
let desc = null
|
|
320
|
+
for (const svgNode of svg.children) {
|
|
321
|
+
if (svgNode instanceof SVGDescElement) {
|
|
322
|
+
desc = svgNode.textContent
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
title, desc
|
|
327
|
+
}
|
|
328
|
+
}, definition, mermaidConfig, myCSS, backgroundColor, svgId, iconPacks)
|
|
329
|
+
|
|
330
|
+
if (outputFormat === 'svg') {
|
|
331
|
+
const svgXML = await page.$eval('svg', (svg) => {
|
|
332
|
+
// SVG might have HTML <foreignObject> that are not valid XML
|
|
333
|
+
// E.g. <br> must be replaced with <br/>
|
|
334
|
+
// Luckily the DOM Web API has the XMLSerializer for this
|
|
335
|
+
// eslint-disable-next-line no-undef
|
|
336
|
+
const xmlSerializer = new XMLSerializer()
|
|
337
|
+
return xmlSerializer.serializeToString(svg)
|
|
338
|
+
})
|
|
339
|
+
return {
|
|
340
|
+
...metadata,
|
|
341
|
+
data: new TextEncoder().encode(svgXML)
|
|
342
|
+
}
|
|
343
|
+
} else if (outputFormat === 'png') {
|
|
344
|
+
const clip = await page.$eval('svg', svg => {
|
|
345
|
+
const react = svg.getBoundingClientRect()
|
|
346
|
+
return { x: Math.floor(react.left), y: Math.floor(react.top), width: Math.ceil(react.width), height: Math.ceil(react.height) }
|
|
347
|
+
})
|
|
348
|
+
await page.setViewport({ ...viewport, width: clip.x + clip.width, height: clip.y + clip.height })
|
|
349
|
+
return {
|
|
350
|
+
...metadata,
|
|
351
|
+
data: await page.screenshot({ clip, omitBackground: backgroundColor === 'transparent' })
|
|
352
|
+
}
|
|
353
|
+
} else { // pdf
|
|
354
|
+
if (pdfFit) {
|
|
355
|
+
const clip = await page.$eval('svg', svg => {
|
|
356
|
+
const react = svg.getBoundingClientRect()
|
|
357
|
+
return { x: react.left, y: react.top, width: react.width, height: react.height }
|
|
358
|
+
})
|
|
359
|
+
return {
|
|
360
|
+
...metadata,
|
|
361
|
+
data: await page.pdf({
|
|
362
|
+
omitBackground: backgroundColor === 'transparent',
|
|
363
|
+
width: (Math.ceil(clip.width) + clip.x * 2) + 'px',
|
|
364
|
+
height: (Math.ceil(clip.height) + clip.y * 2) + 'px',
|
|
365
|
+
pageRanges: '1-1'
|
|
366
|
+
})
|
|
367
|
+
}
|
|
368
|
+
} else {
|
|
369
|
+
return {
|
|
370
|
+
...metadata,
|
|
371
|
+
data: await page.pdf({
|
|
372
|
+
omitBackground: backgroundColor === 'transparent'
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
} finally {
|
|
378
|
+
await page.close()
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* @typedef {object} MarkdownImageProps Markdown image properties
|
|
384
|
+
* Used to create a markdown image that looks like ``
|
|
385
|
+
* @property {string} url - Path to image.
|
|
386
|
+
* @property {string} alt - Image alt text, required.
|
|
387
|
+
* @property {string | null} [title] - Optional image title text.
|
|
388
|
+
*/
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Creates a markdown image syntax.
|
|
392
|
+
*
|
|
393
|
+
* @param {MarkdownImageProps} params - Parameters.
|
|
394
|
+
* @returns {``} The markdown image text.
|
|
395
|
+
*/
|
|
396
|
+
function markdownImage ({ url, title, alt }) {
|
|
397
|
+
// we can't use String.prototype.replaceAll since it's not supported in Node v14
|
|
398
|
+
const altEscaped = alt.replace(/[[\]\\]/g, '\\$&')
|
|
399
|
+
if (title) {
|
|
400
|
+
const titleEscaped = title.replace(/["\\]/g, '\\$&')
|
|
401
|
+
return ``
|
|
402
|
+
} else {
|
|
403
|
+
return ``
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Renders a mermaid diagram or mermaid markdown file.
|
|
409
|
+
*
|
|
410
|
+
* @param {`${string}.${"md" | "markdown"}` | string | undefined} input - If this ends with `.md`/`.markdown`,
|
|
411
|
+
* path to a markdown file containing mermaid.
|
|
412
|
+
* If this is a string, loads the mermaid definition from the given file.
|
|
413
|
+
* If this is `undefined`, loads the mermaid definition from stdin.
|
|
414
|
+
* @param {`${string}.${"md" | "markdown" | "svg" | "png" | "pdf"}` | "/dev/stdout"} output - Path to the output file.
|
|
415
|
+
* @param {Object} [opts] - Options
|
|
416
|
+
* @param {import("puppeteer").LaunchOptions} [opts.puppeteerConfig] - Puppeteer launch options.
|
|
417
|
+
* @param {boolean} [opts.quiet] - If set, suppress log output.
|
|
418
|
+
* @param {"svg" | "png" | "pdf"} [opts.outputFormat] - Mermaid output format.
|
|
419
|
+
* @param {string} [opts.artefacts] - Path to the artefacts directory.
|
|
420
|
+
* Defaults to `output` extension. Overrides `output` extension if set.
|
|
421
|
+
* @param {ParseMDDOptions} [opts.parseMMDOptions] - Options to pass to {@link parseMMDOptions}.
|
|
422
|
+
*/
|
|
423
|
+
async function run (input, output, { puppeteerConfig = {}, quiet = false, outputFormat, parseMMDOptions, artefacts } = {}) {
|
|
424
|
+
/**
|
|
425
|
+
* Logs the given message to stdout, unless `quiet` is set to `true`.
|
|
426
|
+
*
|
|
427
|
+
* @param {string} message - The message to maybe log.
|
|
428
|
+
*/
|
|
429
|
+
const info = message => {
|
|
430
|
+
if (!quiet) {
|
|
431
|
+
console.info(message)
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// TODO: should we use a Markdown parser like remark instead of rolling our own parser?
|
|
436
|
+
const mermaidChartsInMarkdown = /^[^\S\n]*[`:]{3}(?:mermaid)([^\S\n]*\r?\n([\s\S]*?))[`:]{3}[^\S\n]*$/
|
|
437
|
+
const mermaidChartsInMarkdownRegexGlobal = new RegExp(mermaidChartsInMarkdown, 'gm')
|
|
438
|
+
/**
|
|
439
|
+
* @type {puppeteer.Browser | undefined}
|
|
440
|
+
* Lazy-loaded browser instance, only created when needed.
|
|
441
|
+
*/
|
|
442
|
+
let browser
|
|
443
|
+
try {
|
|
444
|
+
if (!outputFormat) {
|
|
445
|
+
const outputFormatFromFilename =
|
|
446
|
+
/**
|
|
447
|
+
* @type {"md" | "markdown" | "svg" | "png" | "pdf"}
|
|
448
|
+
*/ (path.extname(output).replace('.', ''))
|
|
449
|
+
if (outputFormatFromFilename === 'md' || outputFormatFromFilename === 'markdown') {
|
|
450
|
+
// fallback to svg in case no outputFormat is given and output file is MD
|
|
451
|
+
outputFormat = 'svg'
|
|
452
|
+
} else {
|
|
453
|
+
outputFormat = outputFormatFromFilename
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (!/(?:svg|png|pdf)$/.test(outputFormat)) {
|
|
457
|
+
throw new Error('Output format must be one of "svg", "png" or "pdf"')
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const definition = await getInputData(input)
|
|
461
|
+
if (input && /\.(md|markdown)$/.test(input)) {
|
|
462
|
+
if (output === '/dev/stdout') {
|
|
463
|
+
throw new Error('Cannot use `stdout` with markdown input')
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const imagePromises = []
|
|
467
|
+
for (const mermaidCodeblockMatch of definition.matchAll(mermaidChartsInMarkdownRegexGlobal)) {
|
|
468
|
+
if (browser === undefined) {
|
|
469
|
+
browser = await puppeteer.launch(puppeteerConfig)
|
|
470
|
+
}
|
|
471
|
+
const mermaidDefinition = mermaidCodeblockMatch[2]
|
|
472
|
+
|
|
473
|
+
/** Output can be either a template image file, or a `.md` output file.
|
|
474
|
+
* If it is a template image file, use that to created numbered diagrams
|
|
475
|
+
* I.e. if "out.png", use "out-1.png", "out-2.png", etc
|
|
476
|
+
* If it is an output `.md` file, use that to base .svg numbered diagrams on
|
|
477
|
+
* I.e. if "out.md". use "out-1.svg", "out-2.svg", etc
|
|
478
|
+
* @type {string}
|
|
479
|
+
*/
|
|
480
|
+
let outputFile = output.replace(
|
|
481
|
+
/(\.(md|markdown|png|svg|pdf))$/,
|
|
482
|
+
`-${imagePromises.length + 1}$1`
|
|
483
|
+
).replace(/\.(md|markdown)$/, `.${outputFormat}`)
|
|
484
|
+
|
|
485
|
+
if (artefacts) {
|
|
486
|
+
outputFile = path.resolve(artefacts, path.basename(outputFile))
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const outputFileRelative = `./${path.relative(path.dirname(path.resolve(output)), path.resolve(outputFile))}`
|
|
490
|
+
|
|
491
|
+
const imagePromise = (async () => {
|
|
492
|
+
const { title, desc, data } = await renderMermaid(browser, mermaidDefinition, outputFormat, parseMMDOptions)
|
|
493
|
+
await fs.promises.writeFile(outputFile, data)
|
|
494
|
+
info(` ✅ ${outputFileRelative}`)
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
url: outputFileRelative,
|
|
498
|
+
title,
|
|
499
|
+
alt: desc
|
|
500
|
+
}
|
|
501
|
+
})()
|
|
502
|
+
imagePromises.push(imagePromise)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (imagePromises.length) {
|
|
506
|
+
info(`Found ${imagePromises.length} mermaid charts in Markdown input`)
|
|
507
|
+
} else {
|
|
508
|
+
info('No mermaid charts found in Markdown input')
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const images = await Promise.all(imagePromises)
|
|
512
|
+
|
|
513
|
+
if (/\.(md|markdown)$/.test(output)) {
|
|
514
|
+
const outDefinition = definition.replace(mermaidChartsInMarkdownRegexGlobal, (_mermaidMd) => {
|
|
515
|
+
// pop first image from front of array
|
|
516
|
+
const { url, title, alt } =
|
|
517
|
+
/**
|
|
518
|
+
* @type {MarkdownImageProps} We use the same regex,
|
|
519
|
+
* so we will never try to get too many objects from the array.
|
|
520
|
+
* (aka `images.shift()` will never return `undefined`)
|
|
521
|
+
*/ (images.shift())
|
|
522
|
+
return markdownImage({ url, title, alt: alt || 'diagram' })
|
|
523
|
+
})
|
|
524
|
+
await fs.promises.writeFile(output, outDefinition, 'utf-8')
|
|
525
|
+
info(` ✅ ${output}`)
|
|
526
|
+
}
|
|
527
|
+
} else {
|
|
528
|
+
info('Generating single mermaid chart')
|
|
529
|
+
browser = await puppeteer.launch(puppeteerConfig)
|
|
530
|
+
const { data } = await renderMermaid(browser, definition, outputFormat, parseMMDOptions)
|
|
531
|
+
await output !== '/dev/stdout'
|
|
532
|
+
? fs.promises.writeFile(output, data)
|
|
533
|
+
: process.stdout.write(data)
|
|
534
|
+
}
|
|
535
|
+
} finally {
|
|
536
|
+
await browser?.close?.()
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export { run, renderMermaid, cli, error }
|
package/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '11.
|
|
1
|
+
export const version = '11.12.0'
|